package com.xiliubit.cloudpicture.manager;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.xiliubit.cloudpicture.exception.BusinessException;
import com.xiliubit.cloudpicture.exception.ErrorCode;
import com.xiliubit.cloudpicture.model.dto.picture.PictureQueryRequest;
import com.xiliubit.cloudpicture.model.vo.PictureVO;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Component
@Slf4j
public class CacheManager {

    // 最大重试次数
    private static final int MAX_RETRY_TIMES = 3;
    // 初始等待时间（毫秒）
    private static final long BASE_DELAY_MS = 10;
    // 最大等待时间
    private static final long MAX_DELAY_MS = 200;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    /**
     * 本地缓存
     */
    private final Cache<String, String> LOCAL_CACHE =
            Caffeine.newBuilder().initialCapacity(1024)
                    .maximumSize(10000L)
                    // 缓存 5 分钟移除
                    .expireAfterWrite(5L, TimeUnit.MINUTES)
                    .build();


    /**
     * 多层缓存图片列表查询(用户)
     * @param pictureQueryRequest 查询条件
     * @param doDataBaseSelect 数据库查询逻辑
     * @return 图片列表
     */
    public Page<PictureVO> doPageCache (PictureQueryRequest pictureQueryRequest, String prefixKey
                                        ,Supplier<Page<PictureVO>> doDataBaseSelect) {
        // 构造参数
        Page<PictureVO> pictureVOPage = null;

        // 使用多级缓存
        // 构建缓存key
        //  这里暂时用JSON字符串作为缓存key，后期可以改成存储二进制
        // 已经解决缓存击穿问题（Redisson）
        String queryCondition = JSONUtil.toJsonStr(pictureQueryRequest);
        // 利用md5（哈希算法）来压缩key
        String hashKey = DigestUtils.md5DigestAsHex(queryCondition.getBytes());

        // 查询本地缓存
        String localKey = String.format("%s:%s", prefixKey, hashKey);
        // 本地缓存存在，直接返回
        String localCacheValue = LOCAL_CACHE.getIfPresent(localKey);
        if (localCacheValue != null){
            log.info("命中本地缓存");
            pictureVOPage = JSONUtil.toBean(localCacheValue, Page.class);
            return pictureVOPage;
        }

        // 本地缓存不存在，则查询Redis分布式缓存
        //prefixKey = "listPictureVOByPage"  cloudpicture:listPictureVOByPage:%s
        String redisKey = String.format("cloudpicture:%s:%s", prefixKey, hashKey);
        ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
        String redisCacheValue = valueOps.get(redisKey);
        if (redisCacheValue != null){
            // Redis分布式缓存存在，则先缓存到本地缓存中，返回结果
            log.info("命中Redis缓存");
            LOCAL_CACHE.put(localKey, redisCacheValue);
            pictureVOPage = JSONUtil.toBean(redisCacheValue, Page.class);
            return pictureVOPage;
        }
        long delay = BASE_DELAY_MS;
        for (int i = 0; i < MAX_RETRY_TIMES; i++) {
            // Redis分布式缓存为空，尝试获取分布式锁
            // 获取锁
            RLock lock = redissonClient.getLock(localKey);
            try {
                // 尝试加锁，最多等待 10 秒，锁自动释放时间 30 秒（防止死锁）
                boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
                if (isLocked) {
                    log.info("获取锁成功: {}", localKey);
                    // 双重检查
                    redisCacheValue = valueOps.get(redisKey);
                    if (redisCacheValue != null){
                        // Redis分布式缓存存在，则先缓存到本地缓存中，返回结果
                        log.info("命中Redis缓存");
                        LOCAL_CACHE.put(localKey, redisCacheValue);
                        pictureVOPage = JSONUtil.toBean(redisCacheValue, Page.class);
                        return pictureVOPage;
                    }
                    // 未命中缓存 处理缓存逻辑
                    log.info("未命中缓存");
                    // Redis分布式缓存不存在，则查询数据库
                    pictureVOPage = doDataBaseSelect.get();
                    // 结果缓存到Redis缓存和本地缓存中
                    String cacheValue = JSONUtil.toJsonStr(pictureVOPage);
                    // Redis: 4 - 7 分钟随机过期，防止缓存雪崩
                    int cacheExpireTime = 240 + RandomUtil.randomInt(0,120);
                    // 这里不管有没有查到数据，都缓存，防止缓存穿透
                    valueOps.set(redisKey, cacheValue, cacheExpireTime, TimeUnit.SECONDS);
                    LOCAL_CACHE.put(localKey, cacheValue);
                    return pictureVOPage;
                }
            } catch (InterruptedException e) {
                log.error("获取锁异常: {}", localKey);
                // 恢复中断状态
                Thread.currentThread().interrupt();
                // 发生异常时，降级到直接查询数据库可能会导致缓存击穿，改为抛出异常
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "加载失败");
            } finally {
                // 释放锁（必须放在 finally，防止异常时锁未释放）
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
            // 未获取到锁，进行指数退避
            log.info("获取锁失败，资源正忙: {}", localKey);
            // 获取锁失败，短暂休眠后重试（防止惊群）不直接递归，可用指数退避
            try {
                Thread.sleep(delay);
            } catch (InterruptedException e) {
                throw new BusinessException(ErrorCode.OPERATION_ERROR, "加载失败");
            }
            delay = Math.min(delay * 2, MAX_DELAY_MS); // 指数增长，但不超过上限
        }
        // 重试次数用完，抛出异常
        throw new BusinessException(ErrorCode.OPERATION_ERROR, "加载失败");
    }

    /**
     * 检测用户头像上传次数
     *
     * @param userId 用户id
     * @return true: 可以上传，false: 不可以上传
     */
    public boolean checkTimesOfAvatarUpload(Long userId) {
        String key = String.format("cloudpicture:avatarUploadTimes:%s", userId);
        ValueOperations<String, String> valueOps = stringRedisTemplate.opsForValue();
        String avatarUploadTimes = valueOps.get(key);
        if (avatarUploadTimes == null) {
            // 缓存不存在，则设置缓存
            valueOps.set(key, "1", 24, TimeUnit.HOURS);
            return true;
        }
        int times = Integer.parseInt(avatarUploadTimes);
        if (times < 2) {
            // 缓存存在，且次数小于2，则返回true
            valueOps.increment(key);
            return true;
        }
        return false;
    }


    /**
     * 使用SCAN命令清除所有以指定前缀开头的缓存项（推荐用于生产环境）
     * @param prefix 缓存键前缀
     */
    public void clearCacheByPrefixWithScan(String prefix) {
        log.info("开始清理缓存");
        // 清理Redis里的缓存
        stringRedisTemplate.execute((RedisCallback<Object>) connection -> {
            String redisKey = String.format("cloudpicture:%s:*", prefix);
            Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match(redisKey).count(1000).build());
            List<byte[]> keysToDelete = new ArrayList<>();
            while (cursor.hasNext()) {
                keysToDelete.add(cursor.next());
            }
            if (!keysToDelete.isEmpty()) {
                connection.del(keysToDelete.toArray(new byte[0][]));
            }
            cursor.close();
            return null;
        });
        // 清理本地缓存
        LOCAL_CACHE.asMap().keySet().removeIf(key -> key.startsWith(prefix));
    }
}
